Saturday, July 19, 2008

Firefox Greasemonkey Outlook Web Access Extension Part III

Other posts on OWAX: [ Part I | Part II ]

I have reverse engineered the way Microsoft Outlook Web Access uses Ajax to mark records read / unread under Internet Exploder and have added similar functionality to my Firefox Greasemonkey Outlook Web Access Extensions.

The code, also shown below, can be downloaded here.

// ==UserScript==
// @name Outlook Web Access Extensions
// @namespace http://www2.hawaii.edu/~dburger
// @description Extensions to using the bastard child Outlook Web Access
// ==/UserScript==
(
function() {
var UNREAD_IMG = '/exchweb/img/icon-msg-unread.gif';
var READ_IMG = '/exchweb/img/icon-msg-read.gif';
function walkTheDom(node, func) {
func(node);
node = node.firstChild;
while(node) {
walkTheDom(node, func);
node = node.nextSibling;
}
}
function getToolbarTable() {
var tables = document.getElementsByTagName('table');
for (var i = 0; i < tables.length; i++) {
var table = tables[i];
if (table.className === 'trToolbar') return table;
}
return null;
}
function getFindNamesForm() {
var forms = document.getElementsByTagName('form');
return (forms.length == 1 && forms[0].name == 'galfind') ? forms[0] : null;
}
function getBaseHref() {
var bases = document.getElementsByTagName('base');
return (bases) ? bases[0].href : null;
}
function addToolbarButton(toolbarTable, text, func) {
var row = toolbarTable.tBodies[0].rows[0];
var newCell = row.insertCell(row.cells.length - 1);
newCell.setAttribute('valign', 'middle');
newCell.setAttribute('nowrap', 'nowrap');
var font = document.createElement('font');
font.setAttribute('size', '2');
font.appendChild(document.createTextNode(text));
var nobr = document.createElement('nobr');
nobr.appendChild(font);
var a = document.createElement('a');
a.href = 'javascript:void(0);';
a.addEventListener('click', func, true);
a.appendChild(nobr);
newCell.appendChild(a);
}
function changeSelectState(state) {
var inputs = document.getElementsByTagName('input');
for (var i = 0; i < inputs.length; i++) {
var input = inputs[i];
var evt = document.createEvent('MouseEvents');
if (input.type == 'checkbox' && input.checked != state) {
// this won't fire the events to color the row
// input.checked = true;
// so dispatch as an event instead
evt.initEvent('click', true, false);
input.dispatchEvent(evt);
}
}
}
function addSelectAll(toolbarTable) {
addToolbarButton(toolbarTable, 'Select All', function() {
changeSelectState(true);
});
}
function addSelectNone(toolbarTable) {
addToolbarButton(toolbarTable, 'Select None', function () {
changeSelectState(false);
});
}
function setProps(sXml, loadFunc, errorFunc) {
GM_xmlhttpRequest({
method: 'BPROPPATCH',
url: getBaseHref(),
headers: {
'Accept-Language': 'en-us',
'Content-Type': 'text/xml'
},
data: sXml,
onload: loadFunc,
onerror: errorFunc
});
}
function changeReadStatus(status) {
var selInputs = [];
var inputs = document.getElementsByTagName('input');
for (var i = 0; i < inputs.length; i++) {
var input = inputs[i];
if (input.type == 'checkbox' && input.checked) selInputs.push(input);
}
if (selInputs.length > 0) {
var sFlags = 'd:flags="1"'; // suppress receipt
var sXml = '<?xml version="1.0"?\><a:propertyupdate xmlns:a="DAV:"><a:set><a:prop><d:read xmlns:d="urn:schemas:httpmail:" ' + sFlags + '>';
sXml += (status) ? '1':'0';
sXml += '</d:read></a:prop></a:set><a:target>';
for (var i = 0; i < selInputs.length; i++) {
var input = selInputs[i];
sXml += '<a:href>' + getBaseHref() + input.value + '</a:href>';
}
sXml += '</a:target></a:propertyupdate>';
var loadFunc = function(res) {
var parser = new DOMParser();
var doc = parser.parseFromString(res.responseText, 'text/xml');
var responses = doc.getElementsByTagNameNS('DAV:', 'response');
var baseHrefLength = getBaseHref().length;
for (var i = 0; i < responses.length; i++) {
var response = responses[i];
var href = response.getElementsByTagNameNS('DAV:', 'href')[0];
var id = href.firstChild.nodeValue.substring(baseHrefLength - 1);
for (var j = 0; j < selInputs.length; j++) {
var input = selInputs[j];
if (input.type == 'checkbox' && input.value == id) {
var tr = input.parentNode.parentNode;
walkTheDom(tr, function(node) {
if (node.nodeType == 3) {
var text = node.nodeValue;
var parent = node.parentNode;
var grandParent = parent.parentNode;
if (status && parent.tagName == 'B') {
grandParent.removeChild(parent);
var font = document.createElement('font');
font.size = 2;
font.color = 'black';
font.appendChild(document.createTextNode(text));
grandParent.appendChild(font);
} else if (!status && parent.tagName == 'FONT') {
grandParent.removeChild(parent);
var b = document.createElement('b');
b.appendChild(document.createTextNode(text));
grandParent.appendChild(b);
}
} else if (node.nodeType == 1 && node.tagName == 'IMG') {
if (status && node.src.match(UNREAD_IMG + '$')) {
node.src = READ_IMG;
} else if (!status && node.src.match(READ_IMG + '$')) {
node.src = UNREAD_IMG;
}
}
});
}
}
}
};
var errorFunc = function(res) {
alert('Change of read status failed, code: ' + res.status);
};
setProps(sXml, loadFunc, errorFunc);
}
}
function addMarkRead(row) {
addToolbarButton(toolbarTable, 'R', function() {
changeReadStatus(true);
});
}
function addMarkUnread(row) {
addToolbarButton(toolbarTable, 'U', function() {
changeReadStatus(false);
});
}
var toolbarTable = getToolbarTable();
if (toolbarTable) {
addSelectAll(toolbarTable);
addSelectNone(toolbarTable);
addMarkRead(toolbarTable);
addMarkUnread(toolbarTable);
}
// if this is the find names popup form let <enter> work as submit
var findNamesForm = getFindNamesForm();
if (findNamesForm) {
var inputs = document.getElementsByTagName('input');
for (var i = 0; i < inputs.length; i++) {
var input = inputs[i];
if (input.type == 'text') {
input.addEventListener('keypress', function(evt) {
if (evt.keyCode == 13) findNamesForm.submit();
}, true);
}
}
}
}
)();
view raw gistfile1.js hosted with ❤ by GitHub

15 comments:

  1. Is there a keyboard shortcut to handle the Contact lookup when you type in the partial contact Name followed by Ctrl + K in the To/CC text fields when you compose a mail.

    ReplyDelete
  2. First of, you freaking rock if you pull this off. This is the ONLY reason why I am still tethered to IE/XP + Outlook.

    That said, I tried this and it didn't work on my setup:

    OWA 2003
    Firefox 3.0.1

    The script installed fine, and Select All/None works. But marking read and unread doesn't change the state of the select messages (tried reloading, etc.)

    I don't have a clue how to troubleshoot, but if you point me in the right direction, I am VERY interested in making this work...

    ReplyDelete
  3. well... that was easy.

    You had a bug on line 113 - it's hard-coded to _your_ OWA server. As a quick workaround, I changed it to my server and the script works great. I'll try to make a good fix and send it to you.

    Thanks for this super cool script!

    ReplyDelete
  4. After a crash course in Greasemonkey and some really cool Firefox DOM inspection tools, that line should use a getBaseHref() ... but you obviously knew that since you wrote the getBaseHref function. ;)

    Thanks again - super cool!

    BTW - do you mind if I continue to hack on this script? There may be some other things I want to do to OWA to make it work better.

    deaston at sbcglobal dot net

    ReplyDelete
  5. @Dan - ugh, thanks for catching my error! I did in fact leave my hard coded URL in there when it should have been using the getBaseHref() function. I have corrected the code above and at the download link. I actually haven't been thinking about OWAX hacks lately because I switched to using fetchmail to forward my mail to a gmail account. Feel free to do anything with the code that you please. I'd be interested in knowing your improvements.

    ReplyDelete
  6. Hi David,

    I've been using your great userscript for over a year now in Firefox, and it has made dealing with my corporate e-mail a lot less painful. Thank you for that! So naturally, when Google Chrome got Greasemonkey support in its latest version, I immediately came over here and installed the script in Chrome. So this is just to let you know that it works fine in Chrome as well. Thanks again!

    ReplyDelete
  7. @Wieland - thanks for that update. I had read that Google Chrome was now supposed to run greasemonkey scripts but no longer have an Outlook Web Access account to test it against. Google must have done a heck of a job making Chrome greasemonkey compatible! Thats cool!

    ReplyDelete
  8. Hi David, and thanks a lot for that script!
    I was looking for the "mark as read/unread" feature for a long time.
    It works great on my corporate OWA and Firefox 3.6

    ReplyDelete
  9. Great little script - thanks - the Read / unread feature makes reading my corporate mail much easier

    ReplyDelete
  10. Great work! Helped me a lot. Had been looking for this for long.

    ReplyDelete
  11. very very nice! you're the man it works like a charm. thanx!
    cheers from Portugal!

    ReplyDelete
  12. It's pity that Mark as unread is not even there after opening a message in OWA.

    Shame on you Microsoft..

    ReplyDelete
  13. Awesome, thanks a million!

    ReplyDelete